package org.mozhu.mboot.core.config.database;

import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.mozhu.mboot.core.config.ApplicationConfiguration;
import org.mozhu.mboot.core.config.ConfigurationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.util.StringUtils;

import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;

@Slf4j
public class DatabasePropertySource extends EnumerablePropertySource<Map<String, String>> {

    private String tableName = "config";
    private String nameColumn = "name";
    private String valueColumn = "value";

    private ConfigurableEnvironment configurableEnvironment;
    private DatabaseConnectionProperties databaseConnectionProperties;
    private ConcurrentMap<String, String> databaseConfiguration = new ConcurrentHashMap<>();

    private ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(new CustomizableThreadFactory("database-configuration-reload-"));

    public DatabasePropertySource(String name, ConfigurableEnvironment configurableEnvironment) {
        super(name, new HashMap<>());
        this.configurableEnvironment = configurableEnvironment;
    }

    public void init() {
        DatabaseConnectionProperties databaseConnectionProperties = resolveDatabaseProperties();
        if (databaseConnectionProperties == null) {
            log.warn("Database configuration not found");
            return;
        }

        this.databaseConnectionProperties = databaseConnectionProperties;
        databaseConfiguration.putAll(reload());
        executorService.scheduleAtFixedRate(() -> {
            try {
                applyReload();
            } catch (Exception e) {
                log.error("database configuration reload failed", e);
            }
        }, 1, 1, TimeUnit.MINUTES);

    }

    public DatabaseConnectionProperties resolveDatabaseProperties() {
        String driverClassName = configurableEnvironment.getProperty(DatabaseConnectionProperties.DRIVER_CLASS_NAME);
        String url = configurableEnvironment.getProperty(DatabaseConnectionProperties.URL);
        String username = configurableEnvironment.getProperty(DatabaseConnectionProperties.USERNAME);
        String password = configurableEnvironment.getProperty(DatabaseConnectionProperties.PASSWORD);
        if (driverClassName == null || url == null || username == null) {
            return null;
        }
        return DatabaseConnectionProperties.builder().driverClassName(driverClassName).url(url).username(username).password(password).build();
    }

    protected void applyReload() {
        Map<String, String> reloadConfiguration = this.reload();
        Set<String> keys = Sets.newHashSet(this.getPropertyNames());
        for (String key : keys) {
            if (reloadConfiguration.containsKey(key)) {
                if (!reloadConfiguration.get(key).equals(this.getProperty(key))) {
                    this.databaseConfiguration.put(key, reloadConfiguration.get(key));
                    this.fireChanges(key);
                }
                reloadConfiguration.remove(key);
            } else {
                this.databaseConfiguration.remove(key);
                this.fireChanges(key);
            }
        }
        for (Map.Entry entry : reloadConfiguration.entrySet()) {
            String key = entry.getKey().toString();
            this.databaseConfiguration.put(key, entry.getValue().toString());
            this.fireChanges(key);
        }
    }

    protected void fireChanges(String key) {
        for (ConfigurationListener listener : ApplicationConfiguration.get().getConfigurationListeners()) {
            listener.notifyUpdate(key);
        }
    }

    public Map<String, String> reload() {
        Connection conn;
        Statement st;
        ResultSet rs;
        Map<String, String> configuration = new HashMap<>();
        try {
            conn = getConnection();
            st = conn.createStatement();
            rs = st.executeQuery("select * from " + tableName);
            while (rs.next()) {
                String name = rs.getString(nameColumn);
                String value = rs.getString(valueColumn);
                configuration.put(name, value);
            }
            close(rs, st, conn);
            return configuration;
        } catch (SQLException e) {
            log.error("Load configuration from database failed", e);
            throw new IllegalStateException("Load configuration from database failed", e);
        }
    }

    public Connection getConnection() {
        try {
            Class.forName(databaseConnectionProperties.getDriverClassName());
            return DriverManager.getConnection(databaseConnectionProperties.getUrl(), databaseConnectionProperties.getUsername(), databaseConnectionProperties.getPassword());
        } catch (Exception e) {
            log.error("Get database connection failed", e);
            throw new IllegalStateException("Get database connection failed", e);
        }
    }

    public static void close(ResultSet rs, Statement st, Connection conn) {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            log.error("ResultSet close failed", e);
        } finally {
            try {
                if (st != null) {
                    st.close();
                }
            } catch (SQLException e) {
                log.error("Statement close failed", e);
            } finally {
                if (conn != null) {
                    try {
                        conn.close();
                    } catch (Exception e) {
                        log.error("Connection close failed", e);
                    }
                }
            }
        }
    }

    @Override
    public Map<String, String> getSource() {
        return databaseConfiguration;
    }

    @Override
    public String[] getPropertyNames() {
        return StringUtils.toStringArray(this.getSource().keySet());
    }

    @Override
    public String getProperty(String name) {
        return this.getSource().get(name);
    }
}
